// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: donggu

#include "coin2/base/serial.h"

#include <experimental/filesystem>
#include <stdexcept>
#include <string>
#include <vector>

#include <google/protobuf/util/json_util.h>
#include <nlohmann/json.hpp>
#include <toml.hpp>

#include "coin2/base/log.h"

// these serialization / deserialization methods throw on failure.

struct json_serializer {
  std::stringstream* s;
  void operator()(toml::boolean v) const {
    *s << v;
    std::cout << v;
  }
  void operator()(toml::integer v) const {
    *s << v;
    std::cout << v;
  }
  void operator()(toml::floating v) const {
    *s << v;
    std::cout << v;
  }
  void operator()(const toml::string& v) const {
    // since toml11 automatically convert string to multiline string that is
    // valid only in TOML, we need to format the string to make it valid in
    // JSON.
    std::cout << v;
    *s << '"' << escape_string(v.str) << '"';
  }
  void operator()(const toml::local_time& v) const { *s << v; }
  void operator()(const toml::local_date& v) const { *s << v; }
  void operator()(const toml::local_datetime& v) const { *s << v; }
  void operator()(const toml::offset_datetime& v) const { *s << v; }
  void operator()(const toml::array& v) {
    std::cout << "[]";
    *s << '[';
    bool is_first = true;
    for (const auto& elem : v) {
      if (!is_first) {
        *s << ", ";
      }
      is_first = false;
      toml::visit(*this, elem);
    }
    *s << ']';
  }
  void operator()(const toml::table& v) {
    std::cout << "{}";
    *s << '{';
    bool is_first = true;
    for (const auto& elem : v) {
      if (!is_first) {
        *s << ", ";
      }
      is_first = false;
      *s << format_key(elem.first) << ':';
      toml::visit(*this, elem.second);
    }
    *s << '}';
  }

  static std::string escape_string(const std::string& s) {
    std::string retval;
    for (const char c : s) {
      switch (c) {
        case '\\': {
          retval += "\\\\";
          break;
        }
        case '\"': {
          retval += "\\\"";
          break;
        }
        case '\b': {
          retval += "\\b";
          break;
        }
        case '\t': {
          retval += "\\t";
          break;
        }
        case '\f': {
          retval += "\\f";
          break;
        }
        case '\n': {
          retval += "\\n";
          break;
        }
        case '\r': {
          retval += "\\r";
          break;
        }
        default: {
          retval += c;
          break;
        }
      }
    }
    return retval;
  }

  static std::string format_key(const std::string& s) {
    const auto* const quote("\"");
    return quote + escape_string(s) + quote;
  }
};

namespace coin2::base {

nlohmann::json path_to_json(const std::experimental::filesystem::path& path) {
  std::ifstream file(path);
  nlohmann::json j;
  file >> j;
  return j;
}
nlohmann::json toml_to_json(const toml::value& doc) {
  std::stringstream s {};
  toml::visit(json_serializer {&s}, doc);
  return nlohmann::json::parse(s.str());
}
template <typename TProto>
nlohmann::json proto_to_json(const TProto& doc) {
  std::string str;
  doc.MessageToJsonString(doc, &str);
  return nlohmann::json::parse(str);
}

toml::value path_to_toml(const std::experimental::filesystem::path& path) {
  return toml::parse(path);
}
toml::value json_to_toml(const nlohmann::json& doc) {
  if (doc.is_null()) {
    throw std::runtime_error("json null cannot be converted to toml");
  }
  if (doc.is_boolean()) {
    return toml::boolean(doc.get<bool>());
  }
  if (doc.is_number()) {
    return toml::floating(doc.get<double>());
  }
  if (doc.is_object()) {
    toml::table table;
    for (auto& el : doc.items()) {
      table[el.key()] = json_to_toml(el.value());
    }
    return table;
  }
  if (doc.is_array()) {
    std::vector<toml::value> arr;
    for (const auto& v : doc) {
      arr.push_back(json_to_toml(v));
    }
    return arr;
  }
  if (doc.is_string()) {
    return toml::string(doc.get<std::string>());
  }
  // unreachable
  return {};
}
template <typename TProto>
toml::value proto_to_toml(const TProto& doc) {
  return json_to_toml(proto_to_json(doc));
}

template <typename TProto>
TProto path_to_proto(const std::experimental::filesystem::path& path) {
  std::ifstream in(path);
  std::string str(dynamic_cast<std::stringstream const&>(std::stringstream() << in.rdbuf()).str());

  TProto proto;
  auto status = google::protobuf::util::JsonStringToMessage(str, &proto);
  CHECK_THROW(status.ok()) << "path_to_proto() Fails to parse the path " << path << "\n"
                           << status;
  return proto;
}
template <typename TProto>
TProto json_to_proto(const nlohmann::json& doc) {
  auto str = doc.dump();

  TProto proto;
  auto status = google::protobuf::util::JsonStringToMessage(str, &proto);
  CHECK_THROW(status.ok()) << "json_to_proto() Fails to convert the json " << str << "\n"
                           << status;
  return proto;
}
template <typename TProto>
TProto toml_to_proto(const toml::value& doc) {
  return json_to_proto<TProto>(toml_to_json(doc));
}

}  // namespace coin2::base
